package com.hucsmn.examsystem.server;

import com.hucsmn.examsystem.common.AnswerSheet;
import com.hucsmn.examsystem.common.Problem;
import com.hucsmn.examsystem.common.User;
import com.hucsmn.examsystem.common.message.Message;
import com.hucsmn.examsystem.common.message.MessageKind;
import com.hucsmn.examsystem.common.message.Token;
import com.hucsmn.examsystem.model.AnswerSheetDatabase;
import com.hucsmn.examsystem.model.ProblemDatabase;
import com.hucsmn.examsystem.model.UserDatabase;
import com.hucsmn.examsystem.session.Session;
import com.hucsmn.examsystem.session.SessionCollection;

import java.io.*;
import java.net.*;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 在线考试系统服务端。
 * <p>
 * 思路：
 * C/S 架构，每一个 Socket 连接只进行一次请求/回复过程，每次客户端请求/服务端回复结束后，服务端会立即关闭连接。
 * 而服务端保存了 Session 会话状态，让客户端发送的多个请求通过一个 Token 令牌来获取这个 Session，
 * 从而实现多个连接之间的状态共享（会话状态主要包括了登录信息、正在进行的考试等等）。
 */
public class ExamServer implements Runnable {
    // 默认值。

    public static final InetAddress ADDRESS;
    public static final int PORT = 8888;
    public static final int THREADS = 2 * Runtime.getRuntime().availableProcessors();
    public static final int TIMEOUT = 5000;
    public static final long SESSION_EXPIRE = SessionCollection.DEFAULT_EXPIRE;
    public static final long CLEAN_INTERVAL = 3600 * 1000;

    static {
        InetAddress address = null;
        try {
            address = Inet4Address.getByAddress(new byte[]{0, 0, 0, 0});
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        ADDRESS = address;
    }

    // 服务端参数。

    private InetAddress address;
    private int port;
    private int threads;
    private int timeout;
    private long sessionExpire;
    private long cleanInterval;

    // 数据库。

    private UserDatabase users;
    private ProblemDatabase problems;
    private AnswerSheetDatabase answerSheets;

    // 运行时会话和线程池状态。

    private SessionCollection sessions;
    private Thread sessionCleaner;
    private ExecutorService pool;

    // 构造函数。

    public ExamServer() {
        this(ADDRESS, PORT);
    }

    public ExamServer(int port) {
        this(ADDRESS, port);
    }

    public ExamServer(InetAddress address, int port) {
        setAddress(address);
        setPort(port);
        setThreads(THREADS);
        setTimeout(TIMEOUT);
        setSessionExpire(SESSION_EXPIRE);
        setCleanInterval(CLEAN_INTERVAL);

        users = new UserDatabase();
        problems = new ProblemDatabase();
        answerSheets = new AnswerSheetDatabase();
    }

    /**
     * 运行在线考试系统服务端。
     */
    @Override
    public void run() {
        sessions = new SessionCollection();
        pool = Executors.newFixedThreadPool(threads);
        sessionCleaner = new Thread(() -> {
            try {
                while (true) {
                    Thread.sleep(cleanInterval);
                    sessions.clean(sessionExpire);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        sessionCleaner.setDaemon(true);
        sessionCleaner.start();

        try (ServerSocket sock = new ServerSocket()) {
            sock.bind(new InetSocketAddress(address, port));
            System.out.println("start listening on " + sock.getInetAddress());
            while (true) {
                Socket request = sock.accept();
                request.setSoTimeout(timeout);
                pool.submit(() -> handle(request));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 独立线程内处理请求。
     *
     * @param request 客户端请求。
     */
    private void handle(Socket request) {
        try (request) {
            ObjectInputStream in = new ObjectInputStream(
                    new BufferedInputStream(request.getInputStream()));
            Object obj = in.readObject();
            if (!(obj instanceof Message<?>)) {
                throw new ClassCastException();
            }
            Message<?> reqMsg = (Message<?>) obj;
            System.out.println(reqMsg.getMessageKind() + " request from " + request.getRemoteSocketAddress());

            Message<?> respMsg;
            try {
                switch (reqMsg.getMessageKind()) {
                    case ADMIN_LOGIN:
                        respMsg = handleAdminLogin(reqMsg);
                        break;
                    case LOGIN:
                        respMsg = handleLogin(reqMsg);
                        break;
                    case LOGOUT:
                        respMsg = handleLogout(reqMsg);
                        break;
                    case UPDATE_SELF:
                        respMsg = handleUpdateSelf(reqMsg);
                        break;
                    case BEGIN_EXAM:
                        respMsg = handleBeginExam(reqMsg);
                        break;
                    case END_EXAM:
                        respMsg = handleEndExam(reqMsg);
                        break;
                    case RECENT_EXAM:
                        respMsg = handleRecentExam(reqMsg);
                        break;
                    case LIST_EXAMS:
                        respMsg = handleListExams(reqMsg);
                        break;
                    case GET_USER:
                        respMsg = handleGetUser(reqMsg);
                        break;
                    case ADD_USER:
                        respMsg = handleAddUser(reqMsg);
                        break;
                    case UPDATE_USER:
                        respMsg = handleUpdateUser(reqMsg);
                        break;
                    case DELETE_USER:
                        respMsg = handleDeleteUser(reqMsg);
                        break;
                    case LIST_USERS:
                        respMsg = handleListUsers(reqMsg);
                        break;
                    case GET_PROBLEM:
                        respMsg = handleGetProblem(reqMsg);
                        break;
                    case ADD_PROBLEM:
                        respMsg = handleAddProblem(reqMsg);
                        break;
                    case UPDATE_PROBLEM:
                        respMsg = handleUpdateProblem(reqMsg);
                        break;
                    case DELETE_PROBLEM:
                        respMsg = handleDeleteProblem(reqMsg);
                        break;
                    case LIST_PROBLEMS:
                        respMsg = handleListProblems(reqMsg);
                        break;
                    default:
                        respMsg = new Message<>(MessageKind.FAIL, "invalid request");
                }
            } catch (IOException e) {
                respMsg = new Message<>(MessageKind.FAIL, e.toString());
            }

            System.out.println(respMsg.getMessageKind() + " response to " + request.getRemoteSocketAddress());
            ObjectOutputStream out = new ObjectOutputStream(
                    new BufferedOutputStream(request.getOutputStream()));
            out.writeObject(respMsg);
            out.flush();
        } catch (IOException | ClassNotFoundException | ClassCastException e) {
            e.printStackTrace();
        }
    }

    // 以下业务部分。

    private Message<?> handleAdminLogin(Message<?> reqMsg) throws IOException {
        Object obj = reqMsg.getData();
        if (!(obj instanceof User)) {
            throw new ExamServerException("bad request data");
        }

        User u0 = (User) obj;
        User u1 = users.get(u0.getName());
        if (u1 == null || u1.getId() != 0 || !Objects.equals(u0.getPass(), u1.getPass())) {
            throw new ExamServerException("incorrect user name or password for administrator");
        }

        Token token = sessions.login(u1.getId(), u1.getId() == 0);
        return new Message<>(MessageKind.SUCCESS, token, sessionExpire);
    }

    private Message<?> handleLogin(Message<?> reqMsg) throws IOException {
        Object obj = reqMsg.getData();
        if (!(obj instanceof User)) {
            throw new ExamServerException("bad request data");
        }

        User u0 = (User) obj;
        User u1 = users.get(u0.getName());
        if (u1 == null || !Objects.equals(u0.getPass(), u1.getPass())) {
            throw new ExamServerException("incorrect user name or password");
        }

        Token token = sessions.login(u1.getId(), u1.getId() == 0);
        return new Message<>(MessageKind.SUCCESS, token, sessionExpire);
    }

    private Message<?> handleLogout(Message<?> reqMsg) throws IOException {
        Token token = reqMsg.getToken();
        if (sessions.access(token).isEmpty()) {
            throw new ExamServerException("invalid token");
        }
        sessions.logout(token);
        return new Message<String>(MessageKind.SUCCESS);
    }

    private Message<?> handleUpdateSelf(Message<?> reqMsg) throws IOException {
        Session session = requireSession(reqMsg.getToken());

        Object obj = reqMsg.getData();
        if (!(obj instanceof User)) {
            throw new ExamServerException("unsupported operand type");
        }

        User user = (User) obj;
        if (!users.update(session.getUid(), user)) {
            throw new ExamServerException("user deleted or repeated user name");
        }
        return new Message<>(MessageKind.SUCCESS);
    }

    private Message<?> handleBeginExam(Message<?> reqMsg) throws IOException {
        Session session = requireSession(reqMsg.getToken());

        Problem[] selectProblems = problems.select(AnswerSheet.DEFAULT_PROBLEMS);
        AnswerSheet answerSheet = new AnswerSheet(0, selectProblems);

        AnswerSheet withoutAnswers = session.beginExam(answerSheet);
        return new Message<>(MessageKind.SUCCESS, withoutAnswers);
    }

    private Message<?> handleEndExam(Message<?> reqMsg) throws IOException {
        Session session = requireSession(reqMsg.getToken());

        Object obj = reqMsg.getData();
        if (obj == null) {
            session.cancelExam();
            return new Message<>(MessageKind.SUCCESS);
        } else if (!(obj instanceof AnswerSheet)) {
            throw new ExamServerException("unsupported operand type");
        }

        AnswerSheet answerSheet = session.endExam((AnswerSheet) obj);
        int aid = answerSheets.add(answerSheet);
        if (aid < 0) {
            throw new ExamServerException("failed to add answer sheet");
        }
        answerSheet.setId(aid);
        session.setRecentAnswerSheet(aid);
        return new Message<>(MessageKind.SUCCESS, answerSheet);
    }

    private Message<?> handleRecentExam(Message<?> reqMsg) throws IOException {
        Session session = requireSession(reqMsg.getToken());

        int aid = session.getRecentAnswerSheet();
        if (aid < 0) {
            throw new ExamServerException("no recent exam recorded in the current session");
        }

        AnswerSheet answerSheet = answerSheets.get(aid);
        if (answerSheet == null) {
            throw new ExamServerException("recent answer sheet has been removed");
        }
        return new Message<>(MessageKind.SUCCESS, answerSheet);
    }

    private Message<?> handleListExams(Message<?> reqMsg) throws IOException {
        int uid;
        Object obj = reqMsg.getData();
        if (obj == null) {
            uid = requireSession(reqMsg.getToken()).getUid();
        } else if (obj instanceof Integer) {
            requireAdminSession(reqMsg.getToken());

            uid = (Integer) obj;
            if (uid < 0) {
                throw new ExamServerException("invalid user id");
            }
        } else if (obj instanceof String) {
            requireAdminSession(reqMsg.getToken());

            String name = (String) obj;
            User user = users.get(name);
            if (user == null) {
                throw new ExamServerException("incorrect user name");
            }
            uid = user.getId();
        } else {
            throw new ExamServerException("unsupported operand type");
        }

        AnswerSheet[] result = answerSheets.select(uid);
        return new Message<>(MessageKind.SUCCESS, result);
    }

    private Message<?> handleGetUser(Message<?> reqMsg) throws IOException {
        requireAdminSession(reqMsg.getToken());

        User user;
        Object obj = reqMsg.getData();
        if (obj instanceof Integer) {
            int uid = (Integer) obj;
            if (uid < 0) {
                throw new ExamServerException("invalid user id");
            }
            user = users.get(uid);
            if (user == null) {
                throw new ExamServerException("incorrect user id");
            }
        } else if (obj instanceof String) {
            String name = (String) obj;
            user = users.get(name);
            if (user == null) {
                throw new ExamServerException("incorrect user name");
            }
        } else {
            throw new ExamServerException("unsupported operand type");
        }
        return new Message<>(MessageKind.SUCCESS, user);
    }

    private Message<?> handleAddUser(Message<?> reqMsg) throws IOException {
        requireAdminSession(reqMsg.getToken());

        Object obj = reqMsg.getData();
        if (!(obj instanceof User)) {
            throw new ExamServerException("unsupported operand type");
        }

        int uid = users.add((User) obj);
        if (uid < 0) {
            throw new ExamServerException("user name exists");
        }
        return new Message<>(MessageKind.SUCCESS, uid);
    }

    private Message<?> handleUpdateUser(Message<?> reqMsg) throws IOException {
        requireAdminSession(reqMsg.getToken());

        Object obj = reqMsg.getData();
        if (!(obj instanceof User)) {
            throw new ExamServerException("unsupported operand type");
        }

        User user = (User) obj;
        if (!users.update(user.getId(), user)) {
            throw new ExamServerException("incorrect user id or repeated user name");
        }
        return new Message<>(MessageKind.SUCCESS);
    }

    private Message<?> handleDeleteUser(Message<?> reqMsg) throws IOException {
        requireAdminSession(reqMsg.getToken());

        Object obj = reqMsg.getData();
        if (obj instanceof Integer) {
            int uid = (Integer) obj;
            if (uid < 0) {
                throw new ExamServerException("invalid user id");
            }
            if (!users.delete(uid)) {
                throw new ExamServerException("incorrect user id");
            }
        } else if (obj instanceof String) {
            String name = (String) obj;
            if (!users.delete(name)) {
                throw new ExamServerException("incorrect user name");
            }
        } else {
            throw new ExamServerException("unsupported operand type");
        }
        return new Message<>(MessageKind.SUCCESS);
    }

    private Message<?> handleListUsers(Message<?> reqMsg) throws IOException {
        requireAdminSession(reqMsg.getToken());

        User[] result = users.select();
        return new Message<>(MessageKind.SUCCESS, result);
    }

    private Message<?> handleGetProblem(Message<?> reqMsg) throws IOException {
        requireAdminSession(reqMsg.getToken());

        Object obj = reqMsg.getData();
        if (!(obj instanceof Integer)) {
            throw new ExamServerException("unsupported operand type");
        }

        Problem problem = problems.get((Integer) obj);
        if (problem == null) {
            throw new ExamServerException("incorrect problem id");
        }
        return new Message<>(MessageKind.SUCCESS, problem);
    }

    private Message<?> handleAddProblem(Message<?> reqMsg) throws IOException {
        requireAdminSession(reqMsg.getToken());

        Object obj = reqMsg.getData();
        if (!(obj instanceof Problem)) {
            throw new ExamServerException("unsupported operand type");
        }

        int pid = problems.add((Problem) obj);
        if (pid < 0) {
            throw new ExamServerException("failed to add problem");
        }
        return new Message<>(MessageKind.SUCCESS, pid);
    }

    private Message<?> handleUpdateProblem(Message<?> reqMsg) throws IOException {
        requireAdminSession(reqMsg.getToken());

        Object obj = reqMsg.getData();
        if (!(obj instanceof Problem)) {
            throw new ExamServerException("unsupported operand type");
        }

        Problem problem = (Problem) obj;
        if (!problems.update(problem.getId(), problem)) {
            throw new ExamServerException("failed to update problem");
        }
        return new Message<>(MessageKind.SUCCESS);
    }

    private Message<?> handleDeleteProblem(Message<?> reqMsg) throws IOException {
        requireAdminSession(reqMsg.getToken());

        Object obj = reqMsg.getData();
        if (!(obj instanceof Integer)) {
            throw new ExamServerException("unsupported operand type");
        }

        Integer pid = (Integer) obj;
        if (!problems.delete(pid)) {
            throw new ExamServerException("failed to delete problem");
        }
        return new Message<>(MessageKind.SUCCESS);
    }

    private Message<?> handleListProblems(Message<?> reqMsg) throws IOException {
        requireAdminSession(reqMsg.getToken());

        Problem[] result = problems.select();
        return new Message<>(MessageKind.SUCCESS, result);
    }

    // 工具方法。

    private Session requireSession(Token token) throws ExamServerException {
        return sessions.access(token)
                .orElseThrow(() -> new ExamServerException("invalid token or login expired"));
    }

    private Session requireAdminSession(Token token) throws ExamServerException {
        Session session = sessions.access(token)
                .orElseThrow(() -> new ExamServerException("invalid token or login expired"));
        if (!session.isAdmin()) {
            throw new ExamServerException("permission denied");
        }
        return session;
    }

    // getter 和 setter。

    public InetAddress getAddress() {
        return address;
    }

    public void setAddress(InetAddress address) {
        if (address == null) {
            throw new NullPointerException();
        }
        this.address = address;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        if (port < 0) {
            throw new IllegalArgumentException();
        }
        this.port = port;
    }

    public int getThreads() {
        return threads;
    }

    public void setThreads(int threads) {
        if (threads < 1) {
            throw new IllegalArgumentException();
        }
        this.threads = threads;
    }

    public int getTimeout() {
        return timeout;
    }

    public void setTimeout(int timeout) {
        if (timeout < 0) {
            throw new IllegalArgumentException();
        }
        this.timeout = timeout;
    }

    public long getSessionExpire() {
        return sessionExpire;
    }

    public void setSessionExpire(long sessionExpire) {
        if (sessionExpire < 1) {
            throw new IllegalArgumentException();
        }
        this.sessionExpire = sessionExpire;
    }

    public long getCleanInterval() {
        return cleanInterval;
    }

    public void setCleanInterval(long cleanInterval) {
        if (cleanInterval < 1) {
            throw new IllegalArgumentException();
        }
        this.cleanInterval = cleanInterval;
    }

    public void loadUsers(File database) throws IOException {
        this.users = UserDatabase.load(database);
    }

    public void saveUsers(File database) throws IOException {
        if (this.users != null) {
            this.users.save(database);
        }
    }

    public void loadProblems(File database) throws IOException {
        this.problems = ProblemDatabase.load(database);
    }

    public void saveProblems(File database) throws IOException {
        if (this.problems != null) {
            this.problems.save(database);
        }
    }

    public void loadAnswerSheets(File database) throws IOException {
        this.answerSheets = AnswerSheetDatabase.load(database);
    }

    public void saveAnswerSheets(File database) throws IOException {
        if (this.answerSheets != null) {
            this.answerSheets.save(database);
        }
    }
}
